We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
/**
* Notifications Page
*
* Full page view of all notifications, sorted by most recent.
*/
'use client';
import { useEffect, useState, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { formatDistanceToNow } from 'date-fns';
import Image from 'next/image';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { Button } from '@/components/ui/button';
import { Card } from '@canadagpt/design-system';
import { useAuth } from '@/contexts/AuthContext';
import { useNotificationsContext, type Notification } from '@/contexts/NotificationsContext';
import { cn } from '@/lib/utils';
import {
Bell,
Loader2,
CheckCheck,
MessageSquare,
UserPlus,
AtSign,
MessageCircle,
Heart,
FileText,
X,
ChevronLeft,
} from 'lucide-react';
const notificationIcons = {
new_message: MessageSquare,
new_follower: UserPlus,
mention: AtSign,
reply: MessageCircle,
like: Heart,
comment: MessageCircle,
system: FileText,
};
const notificationColors = {
new_message: 'text-blue-500 bg-blue-500/10',
new_follower: 'text-green-500 bg-green-500/10',
mention: 'text-purple-500 bg-purple-500/10',
reply: 'text-orange-500 bg-orange-500/10',
like: 'text-red-500 bg-red-500/10',
comment: 'text-yellow-500 bg-yellow-500/10',
system: 'text-gray-500 bg-gray-500/10',
};
interface NotificationCardProps {
notification: Notification;
onNavigate: (notification: Notification) => void;
onDelete: (id: string) => void;
onMarkAsRead: (id: string) => void;
}
function NotificationCard({ notification, onNavigate, onDelete, onMarkAsRead }: NotificationCardProps) {
const Icon = notificationIcons[notification.type];
const colorClass = notificationColors[notification.type];
const timeAgo = formatDistanceToNow(new Date(notification.created_at), { addSuffix: true });
const isUnread = !notification.read_at;
const handleClick = () => {
if (isUnread) {
onMarkAsRead(notification.id);
}
onNavigate(notification);
};
const handleDismiss = (e: React.MouseEvent) => {
e.stopPropagation();
onDelete(notification.id);
};
return (
<Card
className={cn(
'cursor-pointer transition-all hover:shadow-md',
isUnread && 'border-l-4 border-l-blue-500 bg-blue-950/20'
)}
onClick={handleClick}
>
<div className="flex items-start gap-4 p-4">
{/* Icon or Avatar */}
<div className="flex-shrink-0">
{notification.actor?.avatar_url ? (
<div className="relative h-12 w-12 rounded-full">
<Image
src={notification.actor.avatar_url}
alt={notification.actor.display_name}
fill
className="rounded-full object-cover"
/>
<div className={cn('absolute -bottom-1 -right-1 rounded-full p-1.5', colorClass)}>
<Icon className="h-3 w-3" />
</div>
</div>
) : (
<div className={cn('rounded-full p-3', colorClass)}>
<Icon className="h-6 w-6" />
</div>
)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className={cn('text-base font-medium text-text-primary', isUnread && 'font-semibold')}>
{notification.title}
</h3>
<p className="mt-1 text-sm text-text-secondary line-clamp-2">{notification.message}</p>
<p className="mt-2 text-xs text-text-muted">{timeAgo}</p>
</div>
{/* Actions */}
<div className="flex items-center gap-2 flex-shrink-0">
{isUnread && <div className="h-2.5 w-2.5 rounded-full bg-blue-500" />}
<button
onClick={handleDismiss}
className="p-2 rounded-full text-text-muted hover:text-text-primary hover:bg-bg-overlay transition-colors"
title="Dismiss notification"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
</div>
</div>
</Card>
);
}
export default function NotificationsPage() {
const router = useRouter();
const { user, loading: authLoading } = useAuth();
const { notifications, loading, unreadCount, markAsRead, markOneAsRead, deleteNotification } =
useNotificationsContext();
const [page, setPage] = useState(0);
const [hasMore, setHasMore] = useState(true);
const [allNotifications, setAllNotifications] = useState<Notification[]>([]);
const LIMIT = 20;
// Redirect if not logged in
useEffect(() => {
if (!authLoading && !user) {
router.push('/auth/login');
}
}, [user, authLoading, router]);
// Fetch notifications with pagination
const loadMore = useCallback(async () => {
if (!user) return;
try {
const response = await fetch(`/api/notifications?limit=${LIMIT}&offset=${page * LIMIT}`);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
if (data.notifications.length < LIMIT) {
setHasMore(false);
}
if (page === 0) {
setAllNotifications(data.notifications);
} else {
setAllNotifications((prev) => [...prev, ...data.notifications]);
}
} catch (error) {
console.error('Error fetching notifications:', error);
}
}, [user, page]);
useEffect(() => {
loadMore();
}, [loadMore]);
// Update local state when hook notifications change (real-time updates)
useEffect(() => {
if (notifications.length > 0 && page === 0) {
setAllNotifications(notifications);
}
}, [notifications, page]);
const handleNavigate = (notification: Notification) => {
if (notification.related_entity_type && notification.related_entity_id) {
switch (notification.related_entity_type) {
case 'message':
router.push('/messages');
break;
case 'user':
if (notification.actor) {
router.push(`/users/${notification.actor.username}`);
}
break;
case 'post':
router.push(`/forum/posts/${notification.related_entity_id}`);
break;
case 'comment':
router.push(`/forum/posts/${notification.related_entity_id}`);
break;
case 'bill':
router.push(`/bills/${notification.related_entity_id}`);
break;
case 'debate':
router.push(`/debates/${notification.related_entity_id}`);
break;
}
}
};
const handleDelete = async (id: string) => {
await deleteNotification(id);
setAllNotifications((prev) => prev.filter((n) => n.id !== id));
};
const handleMarkAllRead = async () => {
await markAsRead();
setAllNotifications((prev) => prev.map((n) => ({ ...n, read_at: new Date().toISOString() })));
};
if (authLoading) {
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-text-muted" />
</main>
<Footer />
</div>
);
}
if (!user) {
return null;
}
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 page-container max-w-3xl mx-auto">
{/* Back button */}
<button
onClick={() => router.back()}
className="flex items-center gap-2 text-text-secondary hover:text-text-primary mb-6 transition-colors"
>
<ChevronLeft className="h-4 w-4" />
Back
</button>
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<div className="rounded-full bg-accent-red/10 p-3">
<Bell className="h-8 w-8 text-accent-red" />
</div>
<div>
<h1 className="text-2xl font-bold text-text-primary">Notifications</h1>
{unreadCount > 0 && (
<p className="text-sm text-text-secondary">
{unreadCount} unread notification{unreadCount !== 1 ? 's' : ''}
</p>
)}
</div>
</div>
{unreadCount > 0 && (
<Button variant="ghost" onClick={handleMarkAllRead} className="text-sm">
<CheckCheck className="mr-2 h-4 w-4" />
Mark all as read
</Button>
)}
</div>
{/* Notifications List */}
{loading && allNotifications.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16">
<Loader2 className="h-10 w-10 animate-spin text-text-muted mb-4" />
<p className="text-text-secondary">Loading notifications...</p>
</div>
) : allNotifications.length === 0 ? (
<Card className="text-center py-16">
<div className="rounded-full bg-bg-overlay p-4 mx-auto w-fit mb-4">
<Bell className="h-10 w-10 text-text-muted" />
</div>
<h2 className="text-xl font-semibold text-text-primary mb-2">No notifications yet</h2>
<p className="text-text-secondary">
When you receive notifications, they will appear here.
</p>
</Card>
) : (
<div className="space-y-3">
{allNotifications.map((notification) => (
<NotificationCard
key={notification.id}
notification={notification}
onNavigate={handleNavigate}
onDelete={handleDelete}
onMarkAsRead={markOneAsRead}
/>
))}
{/* Load more button */}
{hasMore && (
<div className="text-center pt-4">
<Button
variant="outline"
onClick={() => setPage((p) => p + 1)}
disabled={loading}
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading...
</>
) : (
'Load more'
)}
</Button>
</div>
)}
</div>
)}
</main>
<Footer />
</div>
);
}